﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using WMemoryProfiler.Memory;

namespace WMemoryProfiler
{
    /// <summary>
    /// Managed Debugger which does load sos automatically to allow the execution of managed debugging commands from a nice C# Api.
    /// </summary>
    public class MdbEng : CdbEng
    {
        /// <summary>
        /// Process has .NET 4.5 runtime loaded. This is needed to check for certain changes to sos.dll and the available options
        /// </summary>
        public bool IsNet45SelfDebugging
        {
            get;
            set;
        }

        internal IntPtr FreeMTPtr;

        /// <summary>
        /// Attach debugger to current process
        /// </summary>
        public MdbEng(ProcessWindowStyle debuggerWindowStyle = ProcessWindowStyle.Hidden, DebuggerEvents toIgnore=DebuggerEvents.Default)
            : this(ProcessInfos.SelfPid, debuggerWindowStyle, toIgnore)
        { }

        /// <summary>
        /// Attach debugger to specified process
        /// </summary>
        /// <param name="pid">process id</param>
        /// <param name="debuggerWindowStyle">Optional debugger window style. Default is hidden.</param>
        /// <param name="toIgnore">When the debugger is started several sxn commands are issued to prevent the debugger to break and cause therfore hangs during self debugging from which we cannot recover.</param>
        public MdbEng(int pid, ProcessWindowStyle debuggerWindowStyle = ProcessWindowStyle.Hidden, DebuggerEvents toIgnore = DebuggerEvents.Default)
            : base(pid, debuggerWindowStyle, toIgnore)
        {
            try
            {
                string sos = ProcessInfos.GetSosPath(Process.GetProcessById(pid));
                if (sos == null)
                {
                    throw new InvalidOperationException(String.Format("The application to debug was not recognized as managed application", pid));
                }

                if (IsSelfDebugging)
                {
                    var clrModule = Process.GetCurrentProcess()
                                           .Modules.Cast<ProcessModule>()
                                           .Where(m => String.Equals(Path.GetFileName(m.FileName), "clr.dll", StringComparison.OrdinalIgnoreCase))
                                           .SingleOrDefault();
                    if (clrModule != null)
                    {
                        var version = clrModule.FileVersionInfo;
                        // -live switch is only present at .NET 4.5 and above
                        IsNet45SelfDebugging = (version.FileMajorPart == 4 && version.FileMinorPart == 0 && version.FilePrivatePart > 17000) || 
                                               (clrModule.FileVersionInfo.FileMajorPart >= 4 && version.FileMinorPart >= 6);
                    }

                }

                Execute(String.Format(".load {0}", sos));

                // Pre .NET 4.5 DumpHeap does not support -live which does filter out
                // all Free pseudoobjects. We need therefore to filter Free objects by ourself.
                // For this we do need the MT of the Free object
                if (IsSelfDebugging && CanDebugProcess && !IsNet45SelfDebugging)
                {
                    FreeMTPtr = GetFreeMethodTable();
                }
            }
            catch (Exception)
            {
                // close a potentially already open debugger
                Dispose();
                throw;
            }
        }

        /// <summary>
        /// Start process and attach debugger to it after it has started
        /// </summary>
        /// <param name="fileName"></param>
        /// <param name="args"></param>
        /// <param name="debuggerWindowStyle">Optional debugger window style. Default is hidden.</param>
        public MdbEng(string fileName, string args = "", ProcessWindowStyle debuggerWindowStyle = ProcessWindowStyle.Hidden)
            : this(Process.Start(fileName, args).Id, debuggerWindowStyle)
        {
        }


        /// <summary>
        /// Execute !GCRoot command on object
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        public GCRootInfo GetGCRoot(object obj)
        {
            return new GCRootInfo
            {
                RawOutput = Execute(String.Format("!GCRoot -all {0:X}", ObjectAddress.GetAddress(obj).Key.ToInt64()))
            };
        }

        /// <summary>
        /// Get pseudo object MT for GC holes on the managed heap
        /// </summary>
        /// <returns></returns>
        private IntPtr GetFreeMethodTable()
        {
            var lines = Execute("!DumpHeap -type Free");
            var freeMTLine = lines.Where(line => line.EndsWith(" Free") && !line.Contains("!DumpHeap")).First();
            string freeMTNumber = freeMTLine.Split(new char[] { ' ' })[1];
            return new IntPtr(long.Parse(freeMTNumber, NumberStyles.HexNumber, CultureInfo.InvariantCulture));
        }


        /// <summary>
        /// Retrieve memory statistics. Key is the type name and value the info.
        /// </summary>
        /// <returns>Dictionary with type name as key and the instance statistics data as value</returns>
        public Dictionary<string, InstanceInfo> GetMemoryState()
        {
            Dictionary<string, InstanceInfo> lret = new Dictionary<string, InstanceInfo>();
            if (!CanDebugProcess)
            {
                return lret;
            }
            string[] stats = Execute("!DumpHeap -stat");
            var statData = stats.SkipWhile(line => line.IndexOf("MT    Count    TotalSize Class Name") == -1)
                                .Skip(1)
                                .TakeWhile(line => line != "" && !line.StartsWith("Total"))
                                .ToArray();

            for (int i = 0; i < statData.Length; i++)
            {
                string line = statData[i];
                var parts = line.Split(Space, StringSplitOptions.RemoveEmptyEntries);
                if( parts.Length >= 4)
                {
                    lret[String.Join(" ", parts.Skip(3).ToArray())] = new InstanceInfo
                    {
                        InstanceCount = int.Parse(parts[1], CultureInfo.InvariantCulture),
                        AllocatedBytes = long.Parse(parts[2], CultureInfo.InvariantCulture)
                    };
                }
            }

            return lret;
        }

        static int[] Generations = new int[] { 0, 1, 2 };

        /// <summary>
        /// Get GC collection count for all generations.
        /// </summary>
        int CollectionCount
        {
            get
            {
                int lret = 0;
                for (int i = 0; i < Generations.Length; i++)
                {
                    lret += GC.CollectionCount(Generations[i]);
                }

                return lret;
            }
        }

        char[] Space = new char[] { ' ' };


    }
}
